/**
 * @author Francesco Rosso, matr 592057
 */

package pcd1112.model;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Observable;

import pcd1112.model.eccezioni.EnergyException;
import pcd1112.model.eccezioni.NodoNonPresenteException;
import pcd1112.model.messaggi.Hello;
import pcd1112.model.messaggi.HelloResponse;
import pcd1112.model.messaggi.MessaggioBase;
import pcd1112.model.messaggi.MessaggioBroadcast;
import pcd1112.model.messaggi.MessaggioLSMControllo;
import pcd1112.model.messaggi.MessaggioLocationClaim;
import pcd1112.model.messaggi.MessaggioREDControllo;
import pcd1112.model.messaggi.MessaggioTesto;
import pcd1112.model.messaggi.MessaggioUnicast;

public class Nodo extends Observable implements INodo, Runnable {
	private int idNodo;
	private Coordinata posizione;
	private ArrayList<INodo> vicini;
	private ArrayList<MessaggioBase> coda;
	// private ArrayList<MessaggioBase> memorizzati;
	private Map<Integer, Coordinata> messaggiMemorizzati;
	private int energia = 0;
	private int rand = 0;
	private int numMessaggiRicevuti = 0;
	private int numMessaggiSpediti = 0;
	private int numSignatureChecks = 0;
	private boolean clone = false;
	private boolean cloned = false;

	private Thread thisThread = null;

	/**
	 * 
	 * @param c
	 *            the node position
	 * @param i
	 *            the node id
	 * @param energia
	 *            the node energy
	 */
	public Nodo(Coordinata c, int i, int energia) {
		posizione = c;
		idNodo = i;
		vicini = new ArrayList<INodo>();
		coda = new ArrayList<MessaggioBase>();
		messaggiMemorizzati = new HashMap<Integer, Coordinata>();
		this.energia = energia;
		// setDaemon(true);
	}

	public void run() {
		thisThread = Thread.currentThread();
		Ambiente a = Ambiente.getAmbiente();

		if (Config.getConfig().getCurrentMode() == Config.Mode.real) {
			a.inviaBroadcast(new Hello(this));
		}

		MessaggioBase m = null;
		try {
			while (!a.isStopEverything() && getEnergy() > 0) {
				synchronized (coda) {
					while (coda.isEmpty()) {
						coda.wait();
					}
					m = coda.remove(0);
				}

				// Meanwhile, while we were in wait status, another node could
				// have found the clone
				if (!a.isStopEverything()) {

					// tolgo sempre energia, a meno che non sia un
					// HelloResponse oppure un Hello
					if (!(m instanceof HelloResponse) && !(m instanceof Hello)) {
						try {
							this.consumeEnergy(Config.getConfig()
									.getE_receive());
							numMessaggiRicevuti++;
						} catch (EnergyException e) {
							throw e;
						}
						Thread.sleep(Config.getConfig().getDelay());
					}
					if (m instanceof MessaggioBroadcast) {
						riceviMessaggio((MessaggioBroadcast) m);
					}
					if (m instanceof MessaggioUnicast) {
						riceviMessaggio((MessaggioUnicast) m);
					}

					setChanged();
					notifyObservers("MessageElaborated");

				}
			}
		} catch (EnergyException e) {
		} catch (InterruptedException e) {
		}
		setChanged();
		notifyObservers("TerminatedNode");
	}

	/**
	 * @return the node position
	 */
	public Coordinata getPosizione() {
		return posizione;
	}

	/**
	 * @return the node id
	 */
	public int getIdNodo() {
		return idNodo;
	}

	/**
	 * adds a message to the node's queue
	 */
	public void riceviMessaggio(MessaggioBase m) {
		synchronized (coda) {
			coda.add(m);
			// In realtà è sufficiente il notify(), dato che solo il nodo stesso
			// può essere in attesa sulla propria coda
			coda.notify();
		}
	}

	private void riceviMessaggio(MessaggioBroadcast m) throws EnergyException {
		Ambiente a = Ambiente.getAmbiente();
		if (m instanceof Hello) {
			a.inviaUnicast(new HelloResponse(this, (Hello) m),
					m.getPosMittente());
			try {
				aggiungiVicino(a.getNodo(m.getPosMittente()));
			} catch (NodoNonPresenteException e) {
			}
		}
		if (m instanceof MessaggioLocationClaim) {
			try {
				this.consumeEnergy(Config.getConfig().getE_signature());
				numSignatureChecks++;
			} catch (EnergyException e) {
				throw e;
			}
			// con probabilità 1-p segue tutti i passi g volte
			try {
				if ((1 - Config.getConfig().getP()) <= Math.random()) {
					// ripetere tutti i passi successivi g volte
					int g = (Config.getConfig().getG());

					if (Config.getConfig().getPROTO().equals("RED")) {
						MessageDigest md;
						for (int i = 0; i < g; i++) {
							try {
								// Many thanks to Jacopo
								md = MessageDigest.getInstance("MD5");
								md.reset();
								md.update((byte) (m.getIdMittente() + rand + i));
								byte[] digest = md.digest(); // calculate
								String x_s = "0.", y_s = "0.";
								for (int ix = 0; ix < (digest.length / 2); ix++)
									x_s += Math.abs(digest[ix]);
								for (int iy = (digest.length / 2); iy < digest.length; iy++)
									y_s += Math.abs(digest[iy]);
								Double x = Double.parseDouble(x_s); // x
								// coordinate
								Double y = Double.parseDouble(y_s); // y
								// coordinate
								Coordinata destinazioneMessaggio = new Coordinata(
										x, y);
								MessaggioREDControllo MessaggioDaSpedire = new MessaggioREDControllo(
										this, destinazioneMessaggio);
								// togliere energia per firma messaggio
								this.consumeEnergy(Config.getConfig()
										.getE_signature());
								numSignatureChecks++;
								inoltraMessaggio(MessaggioDaSpedire);
							} catch (NoSuchAlgorithmException e) {
								// Auto-generated catch block
								e.printStackTrace();
							}
						}
					} else {
						for (int i = 0; i < g; i++) {
							Coordinata destinazioneMessaggio = new Coordinata(
									Math.random(), Math.random());
							MessaggioLSMControllo MessaggioDaSpedire = new MessaggioLSMControllo(
									this, destinazioneMessaggio);
							// togliere energia per firma messaggio
							this.consumeEnergy(Config.getConfig()
									.getE_signature());
							numSignatureChecks++;
							inoltraMessaggio(MessaggioDaSpedire);
						}
					}
				}
			} catch (EnergyException e) {
				throw e;
			}
		}
	}

	/**
	 * 
	 * @return the node remaining energy
	 */
	public int getEnergy() {
		return energia;
	}

	/**
	 * Consume a certain amount of energy
	 * 
	 * @param amount
	 * @throws EnergyException
	 *             if there is not enough remaining energy
	 */
	private synchronized void consumeEnergy(int amount) throws EnergyException {
		// if I have enough energy to do that
		if (getEnergy() > amount) {
			energia = energia - amount;
		} else {
			// System.out.println("Qui nodo " + idNodo + ": finito energia");
			throw new EnergyException();
		}
	}

	private void riceviMessaggio(MessaggioUnicast m) throws EnergyException {
		// memorizzare il messaggio e verificare la presenza di cloni
		// considerando i messaggi memorizzati
		// la verifica per ogni messaggio richiede anche la verifica di una
		// firma
		if (m instanceof MessaggioLSMControllo) {
			try {
				this.consumeEnergy(Config.getConfig().getE_signature());
				numSignatureChecks++;
			} catch (EnergyException e) {
				throw e;
			}

			Coordinata temp = messaggiMemorizzati.get(m.getIdMittente());
			if (temp != null) {
				if (!temp.equals(m.getPosMittente())) {
					// if we have found the clone
					System.out.println("Nodo " + idNodo
							+ " ho trovato un clone");
					Ambiente.getAmbiente().setCloneFound(true);
				}
			} else {
				messaggiMemorizzati.put(m.getIdMittente(), m.getPosMittente());
			}
			/*
			 * // TODO sostituire questo ciclo con qualcosa di più efficiente //
			 * TODO togliere energia per controllo firma boolean trovato =
			 * false; for (int i = 0; i < memorizzati.size() && !trovato; i++) {
			 * //se due nodi dicono di avere lo stesso id ma appartengono a
			 * posizioni diverse if (m.getIdMittente() ==
			 * memorizzati.get(i).getIdMittente() && !m.getPosMittente().equals(
			 * memorizzati.get(i).getPosMittente())) { // Ho trovato il clone!
			 * trovato = true;
			 * System.out.println("Nodo "+idNodo+" ho trovato un clone");
			 * Ambiente.getAmbiente().setCloneFound(true); }
			 * 
			 * } memorizzati.add(m);
			 */
		}

		if (m.getPosDestinatario() == posizione) {
			processaComeDestinatario(m);
		} else {
			// System.out.println("Nodo" + idNodo
			// + " Non sono il destinatario del messaggio. "
			// + "Devo Inoltrare");
			inoltraMessaggio(m);
		}
	}

	/**
	 * Forward a unicast message to one of the node's neighbours according to
	 * the routing algorithm. If there are no neighbours to forward the message
	 * to or there are no neighbours close enough to the recipient, the node
	 * will set himself as the recipient
	 */
	public void inoltraMessaggio(MessaggioUnicast m) throws EnergyException {
		if (!vicini.isEmpty()) {
			// scorro la lista di tutti i miei vicini
			// per trovare il nodo più vicino alla destinazione
			INodo nodoVicino = this;
			INodo nodoT;
			// distanza = radq(base^2 * altezza^2)
			double distanzaVicino, baseT, altezzaT, distanzaT;
			baseT = m.getPosDestinatario().getX()
					- nodoVicino.getPosizione().getX();
			altezzaT = m.getPosDestinatario().getY()
					- nodoVicino.getPosizione().getY();
			distanzaVicino = Math.hypot(baseT, altezzaT);
			for (int i = 0; i < vicini.size(); i++) {
				nodoT = vicini.get(i);
				baseT = m.getPosDestinatario().getX()
						- nodoT.getPosizione().getX();
				altezzaT = m.getPosDestinatario().getY()
						- nodoT.getPosizione().getY();
				distanzaT = Math.hypot(baseT, altezzaT);

				if (distanzaT < distanzaVicino) {
					// il nodo i è più vicino tra tutti i nodi vicini[0,...,i-1]
					distanzaVicino = distanzaT;
					nodoVicino = nodoT;
				}
			}

			if (nodoVicino == this) {
				// System.out.println("Nodo " + idNodo
				// + " Non ho trovato vicini con distanza inferiore.");
				processaComeDestinatario(m);
			} else {
				Ambiente a = Ambiente.getAmbiente();
				// togliere energia per l'invio del messaggio
				try {
					this.consumeEnergy(Config.getConfig().getE_send());
					numMessaggiSpediti++;
					// System.out.println("Qui nodo "+idNodo+": inoltro messaggio al vicino con id "+nodoVicino.getIdNodo());
					a.inviaUnicast(m, nodoVicino.getPosizione());
				} catch (EnergyException e) {
					throw e;
				}
			}

		} else {
			// Non ho vicini a cui inoltrare il messaggio
			System.out.println("Qui nodo " + idNodo
					+ ". Non ho vicini a cui inoltrare il messaggio");
			processaComeDestinatario(m);
		}
	}

	/**
	 * The method that the node needs to call when he is considered the
	 * recipient
	 * 
	 * @param m
	 * @throws EnergyException
	 */
	private void processaComeDestinatario(MessaggioUnicast m)
			throws EnergyException {
		Ambiente a = Ambiente.getAmbiente();
		if (m instanceof HelloResponse) {
			try {
				aggiungiVicino(a.getNodo(m.getPosMittente()));
			} catch (NodoNonPresenteException e) {
			}
		}
		if (m instanceof MessaggioTesto) {
			System.out.println("Messaggio di testo con scritto "
					+ ((MessaggioTesto) m).getTesto());
		}
		if (m instanceof MessaggioREDControllo) {
			// energy consumption because of signature check of received message
			try {
				this.consumeEnergy((Config.getConfig().getE_signature()));
				numSignatureChecks++;
			} catch (EnergyException e) {
				throw e;
			}

			Coordinata temp = messaggiMemorizzati.get(m.getIdMittente());
			if (temp != null) {
				// energy comsumption because of signature check of stored
				// message
				try {
					this.consumeEnergy((Config.getConfig().getE_signature()));
					numSignatureChecks++;
				} catch (EnergyException e) {
					throw e;
				}
				if (!temp.equals(m.getPosMittente())) {
					// if we have found the clone
					// System.out.println("Nodo " + idNodo
					// + " ho trovato un clone");
					Ambiente.getAmbiente().setCloneFound(true);
				}
			} else {
				messaggiMemorizzati.put(m.getIdMittente(), m.getPosMittente());
			}

			/*
			 * // TODO sostituire questo ciclo con qualcosa di più efficiente //
			 * TODO togliere energia per controllo firma boolean trovato =
			 * false; for (int i = 0; i < memorizzati.size() && !trovato; i++) {
			 * //se due nodi dicono di avere lo stesso id ma appartengono a
			 * posizioni diverse if (m.getIdMittente() ==
			 * memorizzati.get(i).getIdMittente() && !m.getPosMittente().equals(
			 * memorizzati.get(i).getPosMittente())) { // Ho trovato il clone!
			 * trovato = true;
			 * System.out.println("Nodo "+idNodo+" ho trovato un clone");
			 * Ambiente.getAmbiente().setCloneFound(true); }
			 * 
			 * } memorizzati.add(m);
			 */
		}
	}

	/**
	 * Adds a neighbour to the node. It checks if the neighbour is already
	 * present and if the node ID is different from the current node
	 */
	public synchronized void aggiungiVicino(INodo n) {
		boolean presente = false;
		for (int i = 0; i < vicini.size(); i++) {
			if (vicini.get(i).getIdNodo() == n.getIdNodo()) {
				presente = true;
			}
		}
		// Bisogna impedire che un nodo aggiunga se stesso come vicino
		if (!presente && n.getIdNodo() != this.getIdNodo()) {
			vicini.add(n);
		}
	}

	/**
	 * Starts the RED protocol
	 */
	public void startRED(int rand) {
		Ambiente a = Ambiente.getAmbiente();
		this.rand = rand;
		// togliere energia per firma e per invio
		try {
			this.consumeEnergy(Config.getConfig().getE_signature());
			numSignatureChecks++;
			this.consumeEnergy(Config.getConfig().getE_send());
			numMessaggiSpediti++;
			a.inviaBroadcast(new MessaggioLocationClaim(this));
		} catch (EnergyException e) {
		}

	}

	/**
	 * Starts the LSM protocol
	 */
	public void startLSM() {
		Ambiente a = Ambiente.getAmbiente();
		// togliere energia per firma e per invio
		try {
			this.consumeEnergy(Config.getConfig().getE_signature());
			numSignatureChecks++;
			this.consumeEnergy(Config.getConfig().getE_send());
			numMessaggiSpediti++;
			a.inviaBroadcast(new MessaggioLocationClaim(this));
		} catch (EnergyException e) {
		}
	}

	/**
	 * Returns the node's current state
	 */
	public DescrizioneNodo getStato() {
		DescrizioneNodo d = new DescrizioneNodo();
		d.setIdNodo(idNodo);
		d.setPosizione(posizione);
		d.setNumVicini(vicini.size());
		d.setNumMessaggiRicevuti(numMessaggiRicevuti);
		d.setNumMessaggiSpediti(numMessaggiSpediti);
		d.setNumSignatureChecks(numSignatureChecks);
		d.setNumMessaggiMemorizzati(messaggiMemorizzati.size());
		d.setEnergiaRimasta(getEnergy());
		return d;
	}

	@Override
	public boolean equals(INodo n) {
		return (getPosizione().equals(n.getPosizione()) && getIdNodo() == n
				.getIdNodo());
	}

	/**
	 * Returns true if the current node is the clone (useful for the graphic)
	 */
	@Override
	public boolean isClone() {
		return clone;
	}

	/**
	 * @param clone
	 *            the clone to set
	 */
	@Override
	public void setClone(boolean clone) {
		this.clone = clone;
	}

	/**
	 * Ask to the node to interrupt himself
	 */
	public void interrupt() {
		if (thisThread != null)
			thisThread.interrupt();
	}

	/**
	 * Returns the current thread state.
	 */
	public Thread.State getState() {
		if (thisThread == null)
			return Thread.State.NEW;
		return thisThread.getState();
	}

	/**
	 * Returns true if the current node has been cloned (useful for the
	 * graphics)
	 */
	@Override
	public boolean isCloned() {
		return cloned;
	}

	/**
	 * Set the current node as cloned or not
	 */
	@Override
	public void setCloned(boolean cloned) {
		this.cloned = cloned;
	}

}
